<ruleset name="MediaWiki">
<rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
<exclude name="Generic.ControlStructures.InlineControlStructure" />
- <exclude name="MediaWiki.Commenting.FunctionComment.MissingParamComment" />
<exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationProtected" />
<exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic" />
<exclude name="MediaWiki.Commenting.FunctionComment.MissingParamTag" />
<exclude name="MediaWiki.Commenting.FunctionComment.WrongStyle" />
<exclude name="MediaWiki.Commenting.IllegalSingleLineComment.IllegalSingleLineCommentStart" />
<exclude name="MediaWiki.Commenting.IllegalSingleLineComment.IllegalSingleLineCommentEnd" />
+ <exclude name="MediaWiki.Commenting.LicenseComment.InvalidLicenseTag" />
<exclude name="MediaWiki.ControlStructures.AssignmentInControlStructures.AssignmentInControlStructures" />
<exclude name="MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName" />
<exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment" />
<exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.SingleSpaceBeforeSingleLineComment" />
<exclude name="MediaWiki.Usage.DbrQueryUsage.DbrQueryFound" />
<exclude name="MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage" />
+ <exclude name="MediaWiki.Usage.ForbiddenFunctions.assert" />
<exclude name="MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals" />
<exclude name="MediaWiki.Files.ClassMatchesFilename.WrongCase" />
<exclude name="MediaWiki.Files.ClassMatchesFilename.NotMatch" />
</properties>
</rule>
<rule ref="Generic.Files.LineLength">
- <exclude-pattern>*/languages/messages/Messages*.php</exclude-pattern>
+ <exclude-pattern>*/languages/messages/Messages*\.php</exclude-pattern>
</rule>
<rule ref="PSR2.Methods.MethodDeclaration.Underscore">
- <exclude-pattern>*/includes/StubObject.php</exclude-pattern>
+ <exclude-pattern>*/includes/StubObject\.php</exclude-pattern>
</rule>
<file>.</file>
<arg name="encoding" value="UTF-8"/>
'DeleteAction' => __DIR__ . '/includes/actions/DeleteAction.php',
'DeleteArchivedFiles' => __DIR__ . '/maintenance/deleteArchivedFiles.php',
'DeleteArchivedRevisions' => __DIR__ . '/maintenance/deleteArchivedRevisions.php',
+ 'DeleteAutoPatrolLogs' => __DIR__ . '/maintenance/deleteAutoPatrolLogs.php',
'DeleteBatch' => __DIR__ . '/maintenance/deleteBatch.php',
'DeleteDefaultMessages' => __DIR__ . '/maintenance/deleteDefaultMessages.php',
'DeleteEqualMessages' => __DIR__ . '/maintenance/deleteEqualMessages.php',
"jakub-onderka/php-parallel-lint": "0.9.2",
"jetbrains/phpstorm-stubs": "dev-master#1b9906084d6635456fcf3f3a01f0d7d5b99a578a",
"justinrainbow/json-schema": "~5.2",
- "mediawiki/mediawiki-codesniffer": "16.0.0",
+ "mediawiki/mediawiki-codesniffer": "17.0.0",
"monolog/monolog": "~1.22.1",
"nikic/php-parser": "3.1.3",
"nmred/kafka-php": "0.1.5",
require_once __DIR__ . '/../AutoLoader.php';
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerHookHandler {
use Composer\Semver\Constraint\Constraint;
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerPackageModifier {
<?php
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
*/
class ComposerVersionNormalizer {
* @file
* @ingroup Database
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Daniel Kinzler
*/
abstract class DBAccessBase implements IDBAccessObject {
// Should have been in 1.30
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+ // Should have been in 1.30
[ 'migrateComments' ],
// 1.31
[ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
'patch-user_properties-fix-pk.sql' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+
[ 'migrateComments' ],
[ 'renameIndex', 'l10n_cache', 'lc_lang_key', 'PRIMARY', false,
'patch-l10n_cache-primary-key.sql' ],
// Should have been in 1.30
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+ // Should have been in 1.30
[ 'migrateComments' ],
// 1.31
[ 'changeNullableField', 'protected_titles', 'pt_reason', 'NOT NULL', true ],
[ 'addPgField', 'protected_titles', 'pt_reason_id', 'INTEGER NOT NULL DEFAULT 0' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addPgField', 'image', 'img_description_id', 'INTEGER NOT NULL DEFAULT 0' ],
+
[ 'migrateComments' ],
[ 'addIndex', 'site_stats', 'site_stats_pkey', 'patch-site_stats-pk.sql' ],
[ 'addTable', 'ip_changes', 'patch-ip_changes.sql' ],
[ 'renameIndex', 'user_properties', 'user_properties_user_property', 'PRIMARY', false,
'patch-user_properties-fix-pk.sql' ],
[ 'addTable', 'comment', 'patch-comment-table.sql' ],
+
+ // This field was added in 1.31, but is put here so it can be used by 'migrateComments'
+ [ 'addField', 'image', 'img_description_id', 'patch-image-img_description_id.sql' ],
+
[ 'migrateComments' ],
[ 'renameIndex', 'l10n_cache', 'lc_lang_key', 'PRIMARY', false,
'patch-l10n_cache-primary-key.sql' ],
// Cache getServerId() for 24 hours
const SERVER_ID_CACHE_TTL = 86400;
+ /** @var float Warn if lag estimates are made for transactions older than this many seconds */
+ const LAG_STALE_WARN_THRESHOLD = 0.100;
+
/**
* Additional $params include:
* - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
protected function getLagFromPtHeartbeat() {
$options = $this->lagDetectionOptions;
- if ( $this->trxLevel ) {
+ $staleness = $this->trxLevel
+ ? microtime( true ) - $this->trxTimestamp()
+ : 0;
+ if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
// Avoid returning higher and higher lag value due to snapshot age
// given that the isolation level will typically be REPEATABLE-READ
$this->queryLogger->warning(
}
// Wait on the GTID set (MariaDB only)
$gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
- if ( strpos( $gtidArg, ':' ) !== false ) {
- // MySQL GTIDs, e.g "source_id:transaction_id"
- $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
- } else {
- // MariaDB GTIDs, e.g."domain:server:sequence"
- $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
- }
+ $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
} else {
// Wait on the binlog coordinates
$encFile = $this->addQuotes( $pos->getLogFile() );
- $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
+ $encPos = intval( $pos->pos[1] );
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
}
$row = $res ? $this->fetchRow( $res ) : false;
if ( !$row ) {
- throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
+ throw new DBExpectedError( $this,
+ "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
}
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
* @return MySQLMasterPos|bool
*/
public function getReplicaPos() {
- $now = microtime( true ); // as-of-time *before* fetching GTID variables
-
- if ( $this->useGTIDs() ) {
- // Try to use GTIDs, fallbacking to binlog positions if not possible
- $data = $this->getServerGTIDs( __METHOD__ );
- // Use gtid_current_pos for MariaDB and gtid_executed for MySQL
- foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
- if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
- return new MySQLMasterPos( $data[$name], $now );
- }
+ $now = microtime( true );
+
+ if ( $this->useGTIDs ) {
+ $res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ );
+ $gtidRow = $this->fetchObject( $res );
+ if ( $gtidRow && strlen( $gtidRow->Value ) ) {
+ return new MySQLMasterPos( $gtidRow->Value, $now );
}
}
- $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
- if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
+ $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
+ $row = $this->fetchObject( $res );
+ if ( $row && strlen( $row->Relay_Master_Log_File ) ) {
return new MySQLMasterPos(
- "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
+ "{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}",
$now
);
}
* @return MySQLMasterPos|bool
*/
public function getMasterPos() {
- $now = microtime( true ); // as-of-time *before* fetching GTID variables
-
- $pos = false;
- if ( $this->useGTIDs() ) {
- // Try to use GTIDs, fallbacking to binlog positions if not possible
- $data = $this->getServerGTIDs( __METHOD__ );
- // Use gtid_current_pos for MariaDB and gtid_executed for MySQL
- foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
- if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
- $pos = new MySQLMasterPos( $data[$name], $now );
- break;
- }
- }
- // Filter domains that are inactive or not relevant to the session
- if ( $pos ) {
- $pos->setActiveOriginServerId( $this->getServerId() );
- $pos->setActiveOriginServerUUID( $this->getServerUUID() );
- if ( isset( $data['gtid_domain_id'] ) ) {
- $pos->setActiveDomain( $data['gtid_domain_id'] );
- }
- }
- }
+ $now = microtime( true );
- if ( !$pos ) {
- $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
- if ( $data && strlen( $data['File'] ) ) {
- $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
+ if ( $this->useGTIDs ) {
+ $res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ );
+ $gtidRow = $this->fetchObject( $res );
+ if ( $gtidRow && strlen( $gtidRow->Value ) ) {
+ return new MySQLMasterPos( $gtidRow->Value, $now );
}
}
- return $pos;
- }
-
- /**
- * @return int
- * @throws DBQueryError If the variable doesn't exist for some reason
- */
- protected function getServerId() {
- return $this->srvCache->getWithSetCallback(
- $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ),
- self::SERVER_ID_CACHE_TTL,
- function () {
- $res = $this->query( "SELECT @@server_id AS id", __METHOD__ );
- return intval( $this->fetchObject( $res )->id );
- }
- );
- }
-
- /**
- * @return string|null
- */
- protected function getServerUUID() {
- return $this->srvCache->getWithSetCallback(
- $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ),
- self::SERVER_ID_CACHE_TTL,
- function () {
- $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" );
- $row = $this->fetchObject( $res );
-
- return $row ? $row->Value : null;
- }
- );
- }
-
- /**
- * @param string $fname
- * @return string[]
- */
- protected function getServerGTIDs( $fname = __METHOD__ ) {
- $map = [];
- // Get global-only variables like gtid_executed
- $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname );
- foreach ( $res as $row ) {
- $map[$row->Variable_name] = $row->Value;
- }
- // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
- $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname );
- foreach ( $res as $row ) {
- $map[$row->Variable_name] = $row->Value;
+ $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
+ $row = $this->fetchObject( $res );
+ if ( $row && strlen( $row->File ) ) {
+ return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now );
}
- return $map;
- }
-
- /**
- * @param string $role One of "MASTER"/"SLAVE"
- * @param string $fname
- * @return string[] Latest available server status row
- */
- protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
- return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: [];
+ return false;
}
public function serverIsReadOnly() {
return 'CAST( ' . $field . ' AS SIGNED )';
}
- /*
- * @return bool Whether GTID support is used (mockable for testing)
- */
- protected function useGTIDs() {
- return $this->useGTIDs;
- }
}
class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
* - Binlog-based usage assumes single-source replication and non-hierarchical replication.
* - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
* that GTID sets are complete (e.g. include all domains on the server).
- *
- * @see https://mariadb.com/kb/en/library/gtid/
- * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
*/
class MySQLMasterPos implements DBMasterPos {
- /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
- private $style;
- /** @var string|null Base name of all Binary Log files */
- private $binLog;
- /** @var int[]|null Binary Log position tuple (index number, event number) */
- private $logPos;
- /** @var string[] Map of (server_uuid/gtid_domain_id => GTID) */
- private $gtids = [];
- /** @var int|null Active GTID domain ID */
- private $activeDomain;
- /** @var int|null ID of the server were DB writes originate */
- private $activeServerId;
- /** @var string|null UUID of the server were DB writes originate */
- private $activeServerUUID;
+ /** @var string|null Binlog file base name */
+ public $binlog;
+ /** @var int[]|null Binglog file position tuple */
+ public $pos;
+ /** @var string[] GTID list */
+ public $gtids = [];
/** @var float UNIX timestamp */
- private $asOfTime = 0.0;
-
- const BINARY_LOG = 'binary-log';
- const GTID_MARIA = 'gtid-maria';
- const GTID_MYSQL = 'gtid-mysql';
-
- /** @var int Key name of the binary log index number of a position tuple */
- const CORD_INDEX = 0;
- /** @var int Key name of the binary log event number of a position tuple */
- const CORD_EVENT = 1;
+ public $asOfTime = 0.0;
/**
* @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
protected function init( $position, $asOfTime ) {
$m = [];
if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
- $this->binLog = $m[1]; // ideally something like host name
- $this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ];
- $this->style = self::BINARY_LOG;
+ $this->binlog = $m[1]; // ideally something like host name
+ $this->pos = [ (int)$m[2], (int)$m[3] ];
} else {
$gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
foreach ( $gtids as $gtid ) {
- $components = self::parseGTID( $gtid );
- if ( !$components ) {
+ if ( !self::parseGTID( $gtid ) ) {
throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
}
-
- list( $domain, $pos ) = $components;
- if ( isset( $this->gtids[$domain] ) ) {
- // For MySQL, handle the case where some past issue caused a gap in the
- // executed GTID set, e.g. [last_purged+1,N-1] and [N+1,N+2+K]. Ignore the
- // gap by using the GTID with the highest ending sequence number.
- list( , $otherPos ) = self::parseGTID( $this->gtids[$domain] );
- if ( $pos > $otherPos ) {
- $this->gtids[$domain] = $gtid;
- }
- } else {
- $this->gtids[$domain] = $gtid;
- }
-
- if ( is_int( $domain ) ) {
- $this->style = self::GTID_MARIA; // gtid_domain_id
- } else {
- $this->style = self::GTID_MYSQL; // server_uuid
- }
+ $this->gtids[] = $gtid;
}
if ( !$this->gtids ) {
- throw new InvalidArgumentException( "GTID set cannot be empty." );
+ throw new InvalidArgumentException( "Got empty GTID set." );
}
}
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosByDomain = $this->getActiveGtidCoordinates();
- $thatPosByDomain = $pos->getActiveGtidCoordinates();
+ $thisPosByDomain = $this->getGtidCoordinates();
+ $thatPosByDomain = $pos->getGtidCoordinates();
if ( $thisPosByDomain && $thatPosByDomain ) {
$comparisons = [];
// Check that this has positions reaching those in $pos for all domains in common
}
// Prefer GTID comparisons, which work with multi-tier replication
- $thisPosDomains = array_keys( $this->getActiveGtidCoordinates() );
- $thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() );
+ $thisPosDomains = array_keys( $this->getGtidCoordinates() );
+ $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
if ( $thisPosDomains && $thatPosDomains ) {
// Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
// quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
}
/**
- * @return string|null Base name of binary log files
- * @since 1.31
- */
- public function getLogName() {
- return $this->gtids ? null : $this->binLog;
- }
-
- /**
- * @return int[]|null Tuple of (binary log file number, event number)
- * @since 1.31
- */
- public function getLogPosition() {
- return $this->gtids ? null : $this->logPos;
- }
-
- /**
- * @return string|null Name of the binary log file for this position
- * @since 1.31
+ * @return string|null
*/
public function getLogFile() {
- return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}";
+ return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}";
}
/**
- * @return string[] Map of (server_uuid/gtid_domain_id => GTID)
- * @since 1.31
+ * @return string[]
*/
public function getGTIDs() {
return $this->gtids;
}
/**
- * @param int|null $id @@gtid_domain_id of the active replication stream
- * @since 1.31
+ * @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
*/
- public function setActiveDomain( $id ) {
- $this->activeDomain = (int)$id;
- }
-
- /**
- * @param int|null $id @@server_id of the server were writes originate
- * @since 1.31
- */
- public function setActiveOriginServerId( $id ) {
- $this->activeServerId = (int)$id;
- }
-
- /**
- * @param string|null $id @@server_uuid of the server were writes originate
- * @since 1.31
- */
- public function setActiveOriginServerUUID( $id ) {
- $this->activeServerUUID = $id;
+ public function __toString() {
+ return $this->gtids
+ ? implode( ',', $this->gtids )
+ : $this->getLogFile() . "/{$this->pos[1]}";
}
/**
* @param MySQLMasterPos $pos
* @param MySQLMasterPos $refPos
* @return string[] List of GTIDs from $pos that have domains in $refPos
- * @since 1.31
*/
public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
- return array_values(
- array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() )
- );
+ $gtidsCommon = [];
+
+ $relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused)
+ foreach ( $pos->gtids as $gtid ) {
+ list( $domain ) = self::parseGTID( $gtid );
+ if ( isset( $relevantDomains[$domain] ) ) {
+ $gtidsCommon[] = $gtid;
+ }
+ }
+
+ return $gtidsCommon;
}
/**
* @see https://mariadb.com/kb/en/mariadb/gtid
* @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
- * @return array Map of (server_uuid/gtid_domain_id => integer position); possibly empty
+ * @return array Map of (domain => integer position); possibly empty
*/
- protected function getActiveGtidCoordinates() {
+ protected function getGtidCoordinates() {
$gtidInfos = [];
-
- foreach ( $this->gtids as $domain => $gtid ) {
- list( $domain, $pos, $server ) = self::parseGTID( $gtid );
-
- $ignore = false;
- // Filter out GTIDs from non-active replication domains
- if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
- $ignore |= ( $domain !== $this->activeDomain );
- }
- // Likewise for GTIDs from non-active replication origin servers
- if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
- $ignore |= ( $server !== $this->activeServerId );
- } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
- $ignore |= ( $server !== $this->activeServerUUID );
- }
-
- if ( !$ignore ) {
- $gtidInfos[$domain] = $pos;
- }
+ foreach ( $this->gtids as $gtid ) {
+ list( $domain, $pos ) = self::parseGTID( $gtid );
+ $gtidInfos[$domain] = $pos;
}
return $gtidInfos;
}
/**
- * @param string $id GTID
- * @return array|null [domain ID or server UUID, sequence number, server ID/UUID] or null
+ * @param string $gtid
+ * @return array|null [domain, integer position] or null
*/
- protected static function parseGTID( $id ) {
+ protected static function parseGTID( $gtid ) {
$m = [];
- if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
+ if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
// MariaDB style: <domain>-<server id>-<sequence number>
- return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
- } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
- // MySQL style: <server UUID>:<sequence number>-<sequence number>
- // Normally, the first number should reflect the point (gtid_purged) where older
- // binary logs where purged to save space. When doing comparisons, it may as well
- // be 1 in that case. Assume that this is generally the situation.
- return [ $m[1], (int)$m[2], $m[1] ];
+ return [ (int)$m[1], (int)$m[2] ];
+ } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
+ // MySQL style: <UUID domain>:<sequence number>
+ return [ $m[1], (int)$m[2] ];
}
return null;
/**
* @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
* @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
- * @return array|bool Map of (binlog:<string>, pos:(<integer>, <integer>)) or false
+ * @return array|bool (binlog, (integer file number, integer position)) or false
*/
protected function getBinlogCoordinates() {
- return ( $this->binLog !== null && $this->logPos !== null )
- ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
+ return ( $this->binlog !== null && $this->pos !== null )
+ ? [ 'binlog' => $this->binlog, 'pos' => $this->pos ]
: false;
}
$this->init( $data['position'], $data['asOfTime'] );
}
-
- /**
- * @return string GTID set or <binary log file>/<position> (e.g db1034-bin.000976/843431247)
- */
- public function __toString() {
- return $this->gtids
- ? implode( ',', $this->gtids )
- : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
- }
}
}
// check for roman numbers like VII, XIX...
- $roman = '/^M{0,3}(C[DM]|D{0,1}C{0,3})(X[LC]|L{0,1}X{0,3})(I[VX]|V{0,1}I{0,3})$/u';
+ // Lookahead assertion ensures $roman doesn't match the empty string
+ $roman = '/^(?=[MDCLXVI])M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})$/u';
# match any sub-string of the relevant letters and convert it
$matches = preg_split( '/(\b|^)[^' . $letters . ']+(\b|$)/u',
// Only process words composed of standard English alphabet, leave the rest unchanged.
// This skips some English words like 'naïve' or 'résumé', but we can live with that.
// Ignore single letters and words which aren't lowercase or uppercase-first.
- return preg_replace_callback( '/[A-Za-z][a-z]+/', function ( $matches ) {
+ return preg_replace_callback( '/[A-Za-z][a-z\']+/', function ( $matches ) {
$word = $matches[0];
if ( preg_match( '/^[aeiou]/i', $word ) ) {
return $word . 'way';
} else {
- return preg_replace_callback( '/^(qu|[^aeiou][^aeiouy]*)(.*)$/i', function ( $m ) {
+ return preg_replace_callback( '/^(s?qu|[^aeiou][^aeiouy]*)(.*)$/i', function ( $m ) {
$ucfirst = strtoupper( $m[1][0] ) === $m[1][0];
if ( $ucfirst ) {
return ucfirst( $m[2] ) . lcfirst( $m[1] ) . 'ay';
/* From Kazakh interface, maybe we need it later
$breaks = '[^\w\x80-\xff]';
// regexp for roman numbers
- $roman = 'M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})';
+ // Lookahead assertion ensures $roman doesn't match the empty string
+ $roman = '(?=[MDCLXVI])M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})';
$roman = '';
$reg = '/^'.$roman.'$|^'.$roman.$breaks.'|'.$breaks.$roman.'$|'.$breaks.$roman.$breaks.'/';
$breaks = '[^\w\x80-\xff]';
// regexp for roman numbers
- $roman = 'M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})';
+ // Lookahead assertion ensures $roman doesn't match the empty string
+ $roman = '(?=[MDCLXVI])M{0,4}(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3})';
$reg = '/^' . $roman . '$|^' . $roman . $breaks . '|' . $breaks
. $roman . '$|' . $breaks . $roman . $breaks . '/';
"customjsonprotected": "Вы ня маеце дазволу на рэдагаваньне гэтай JSON-старонкі, таму што яна ўтрымлівае пэрсанальныя налады іншага ўдзельніка.",
"customjsprotected": "Вы ня маеце правоў на рэдагаваньне гэтай старонкі JavaScript, таму што яна ўтрымлівае пэрсанальныя налады іншага ўдзельніка.",
"mycustomcssprotected": "Вы ня маеце дазволу рэдагаваць гэтую CSS-старонку.",
+ "mycustomjsonprotected": "Вы ня маеце дазволу на рэдагаваньне гэтай JSON-старонкі.",
"mycustomjsprotected": "Вы ня маеце дазволу рэдагаваць гэтую JavaScript-старонку.",
"myprivateinfoprotected": "Вы ня маеце дазволу на зьмяненьне ўласных прыватных зьвестак.",
"mypreferencesprotected": "Вы ня маеце дазволу на зьмяненьне сваіх наладаў.",
"prefs-files": "Файлы",
"prefs-custom-css": "Уласны CSS",
"prefs-custom-js": "Уласны JavaScript",
- "prefs-common-config": "Агульны CSS/JavaScript для ўсіх тэмаў афармленьня:",
+ "prefs-common-config": "Агульны CSS/JSON/JavaScript для ўсіх тэмаў афармленьня:",
"prefs-reset-intro": "Вы можаце выкарыстаць гэтую старонку для замены вашых наладаў на налады сайту па змоўчаньні.\nГэтае дзеяньне ня можа быць адмененае.",
"prefs-emailconfirm-label": "Пацьверджаньне адрасу электроннай пошты:",
"youremail": "Адрас электроннай пошты:",
"grant-group-other": "Розная актыўнасьць",
"grant-blockusers": "Блякаваньне і разблякаваньне ўдзельнікаў",
"grant-createaccount": "Стварэньне рахункаў",
- "grant-createeditmovepage": "Ствараць, рэдагаваць і пераносіць старонкі",
+ "grant-createeditmovepage": "Стварэньне, рэдагаваньне і перанос старонак",
"grant-delete": "Выдаляць старонкі, вэрсіі і запісы журналу",
"grant-editinterface": "Рэдагаваць прасторы назваў МэдыяВікі і CSS/JavaScript удзельніка",
"grant-editmycssjs": "Рэдагаваць Ваш CSS/JavaScript",
"Asmen",
"Meliganai",
"Ilimanaq29",
- "Patriccck"
+ "Patriccck",
+ "Ed g2s"
]
},
"tog-underline": "Podtrhávat odkazy:",
"savechanges": "Uložit změny",
"publishpage": "Zveřejnit stránku",
"publishchanges": "Zveřejnit změny",
- "savearticle-start": "Uložit stránku",
- "publishpage-start": "Zveřejnit stránku",
+ "savearticle-start": "Uložit změny…",
+ "savechanges-start": "Uložit změny…",
+ "publishpage-start": "Zveřejnit stránku…",
+ "publishchanges-start": "Zveřejnit změny…",
"preview": "Náhled",
"showpreview": "Ukázat náhled",
"showdiff": "Ukázat změny",
"databaseerror-query": "Rékèt : $1",
"databaseerror-function": "Fonksyon : $1",
"databaseerror-error": "Érò : $1",
+ "transaction-duration-limit-exceeded": "Pou évité roun tròp fò ogmantasyon di délè di réplikasyon, sa tranzaksyon té anilé piskétan douré di ékritir ($1) dépasé limit-a di $2 ségonn. Si zòt ka sasé modifyé oun gran nonm di éléman similtanéman, éséyé plito di éfèktchwé opérasyon-an an plizyò étap pli piti.",
"laggedslavemode": "Panga, sa paj pa pé kontni tout dannyé modifikasyon éfèktchwé",
"readonly": "Baz di doné vérouyé",
"enterlockreason": "Endiké rézon-an di vérouyaj ensi ki roun èstimasyon di so douré",
"readonlytext": "Ajou ké mizajou di baz di doné sa atchwèlman bloké, probabman pou pèrmèt mentnans di baz-a, aprè sa, tout bagaj ké rantré andan lòrd.\n\nAdministratò sistèm ki vérouyé baz di doné té fourni èksplikasyon swivant :<br /> $1",
"missing-article": "Baz-a di doné pa trouvé tèks-a di roun paj ki li té divèt trouvé, entitilé « $1 » $2.\n\nJénéralman, sala ka sirviv an swivan roun lyen vèr roun diff périmé ou vèr listorik di roun paj souprimé.\n\nSi a pa sa ki la, zòt pitèt trouvé roun anomali annan program-an.\nSouplé, signalé li à roun [[Special:ListUsers/sysop|administratò]] é pa bliyé di endiké li URL-a di paj-a.",
"missingarticle-rev": "(niméro di vèrsyon : $1)",
+ "missingarticle-diff": "(diff : $1, $2)",
+ "readonly_lag": "Baz-a di doné té otomatikman vérouyé pannan ki sèrvò-ya ségondèr ka réyaligné yé kò asou sèrvò prensipal",
+ "nonwrite-api-promise-error": "Ankèt-a HTTP « <code>Promise-Non-Write-API-Action:</code> » té voyé mè rékèt-a té fè à oun modjoul di ékritir di API-a.",
+ "internalerror": "Érò entèrn",
+ "internalerror_info": "Érò entèrn : $1",
+ "internalerror-fatal-exception": "Érò fatal di tip « $1 »",
+ "filecopyerror": "Enposib di kopyé fiché-a « $1 » vèr « $2 ».",
+ "filerenameerror": "Enposib di rounonmen fiché-a « $1 » an « $2 ».",
+ "filedeleteerror": "Enposib di souprimé fiché-a « $1 ».",
+ "directorycreateerror": "Enposib di kréyé répèrtwar-a « $1 ».",
+ "directoryreadonlyerror": "Répèrtwar-a « $1 » sa an lèktir sèl.",
+ "directorynotreadableerror": "Répèrtwar-a « $1 » pa lizib.",
+ "filenotfound": "Enposib di trouvé fiché-a « $1 ».",
+ "unexpected": "Valò ki pa nòrmal : « $1 » = « $2 ».",
+ "formerror": "Érò : enposib di soumèt fòrmilèr-a.",
+ "badarticleerror": "Sa aksyon pa pé sa éfèktchwé asou sa paj.",
+ "no-null-revision": "Enposib di kréyé roun nouvèl révizyon vid pou paj-a « $1 »",
"badtitle": "Movè tit",
"badtitletext": "Tit di paj doumandé pa valid, vid, ou mal formé si a roun tit entèr-lanng ou entèr-projè.\nI ka kontni pitèt oun ou plizyò karaktèr ki pa pé sa itilizé andan tit-ya.",
"viewsource": "Wè tèks sours",
"underline-default": "ברירת המחדל של העיצוב או של הדפדפן",
"editfont-style": "הגופן בתיבת העריכה:",
"editfont-monospace": "גופן ברוחב קבוע (monospace)",
- "editfont-sansserif": "×\92×\95פ×\9f ×\9c×\90 ×\9e×¢×\95צ×\91 (sans-serif)",
- "editfont-serif": "×\92×\95פ×\9f ×\9e×¢×\95צ×\91 (serif)",
+ "editfont-sansserif": "×\92×\95פ×\9f ×\9c×\9c×\90 ת×\92×\99×\9d",
+ "editfont-serif": "×\92×\95פ×\9f ×¢×\9d ת×\92×\99×\9d",
"sunday": "ראשון",
"monday": "שני",
"tuesday": "שלישי",
"botpasswords-no-provider": "BotPasswordsSessionProvider אינו זמין.",
"botpasswords-restriction-failed": "כניסה זו נמנעה בשל הגבלות על סיסמאות בוט.",
"botpasswords-invalid-name": "שם המשתמש שניתן אינו מכיל את תו הפרדת סיסמאות הבוט (\"$1\").",
- "botpasswords-not-exist": "למשתמש \"$1\" אין ססמת בוט בשם \"$2\".",
+ "botpasswords-not-exist": "{{GENDER:$1|למשתמש|למשתמשת}} \"$1\" אין סיסמת בוט בשם \"$2\".",
"resetpass_forbidden": "לא ניתן לשנות סיסמאות.",
"resetpass_forbidden-reason": "לא ניתן לשנות את הסיסמאות: $1",
"resetpass-no-info": "נדרשת כניסה לחשבון כדי לגשת לדף זה באופן ישיר.",
"resetpass-submit-loggedin": "שינוי סיסמה",
"resetpass-submit-cancel": "ביטול",
"resetpass-wrong-oldpass": "הסיסמה הזמנית או הנוכחית אינה תקינה.\nייתכן שכבר שינית את סיסמתך או שכבר ביקשת סיסמה זמנית חדשה.",
- "resetpass-recycled": "×\90× ×\90 ×\90פס×\95 ×\90ת ×\94ס×\99ס×\9e×\94 ×\9cס×\99ס×\9e×\94 ש×\95× ×\94 ×\9eס×\99ס×\9eת×\9b×\9d הנוכחית.",
- "resetpass-temp-emailed": "נכנסתם באמצעות סיסמה זמנית שנשלחה אליכם בדוא\"ל.\nכדי לסיים את הכניסה, עליכם להגדיר כאן סיסמה חדשה:",
+ "resetpass-recycled": "×\99ש ×\9c×\90פס ×\90ת ×\94ס×\99ס×\9e×\94 ×\9cס×\99ס×\9e×\94 ×\94ש×\95× ×\94 ×\9eס×\99×\9eסת×\9a הנוכחית.",
+ "resetpass-temp-emailed": "נכנסת באמצעות סיסמה זמנית שנשלחה {{GENDER:|אליך|אלייך}} בדוא\"ל.\nכדי לסיים את הכניסה, יש להגדיר כאן סיסמה חדשה:",
"resetpass-temp-password": "סיסמה זמנית:",
"resetpass-abort-generic": "שינוי הסיסמה בוטל על־ידי הרחבה.",
- "resetpass-expired": "ס×\99ס×\9eת×\9b×\9d פקע×\94. ×\90× ×\90 ×\94×\92×\93×\99ר×\95 סיסמה חדשה כדי להיכנס.",
+ "resetpass-expired": "ס×\99ס×\9eת×\9a פקע×\94. × ×\90 ×\9c×\94×\92×\93×\99ר סיסמה חדשה כדי להיכנס.",
"resetpass-expired-soft": "הסיסמה שלך פקעה, וצריך לאפס אותה. יש לבחור סיסמה חדשה כעת, או ללחוץ על \"{{int:authprovider-resetpass-skip-label}}\" כדי לאפס אותה מאוחר יותר.",
"resetpass-validity-soft": "הסיסמה שלך אינה תקינה: $1\n\nיש לבחור סיסמה חדשה כעת או ללחוץ על \"{{int:authprovider-resetpass-skip-label}}\" כדי לאפס את הסיסמה מאוחר יותר.",
"passwordreset": "איפוס סיסמה",
- "passwordreset-text-one": "×\9e×\9c×\90×\95 טופס זה כדי לקבל סיסמה זמנית בדוא\"ל.",
- "passwordreset-text-many": "{{PLURAL:$1||×\9e×\9c×\90×\95 אחד מהשדות הבאים כדי לקבל סיסמה זמנית בדוא\"ל.}}",
+ "passwordreset-text-one": "× ×\90 ×\9c×\9e×\9c×\90 טופס זה כדי לקבל סיסמה זמנית בדוא\"ל.",
+ "passwordreset-text-many": "{{PLURAL:$1||×\99ש ×\9c×\9e×\9c×\90 אחד מהשדות הבאים כדי לקבל סיסמה זמנית בדוא\"ל.}}",
"passwordreset-disabled": "איפוסי סיסמה בוטלו באתר ויקי זה.",
"passwordreset-emaildisabled": "שירותי הדוא\"ל בוטלו באתר ויקי זה.",
"passwordreset-username": "שם משתמש:",
"passwordreset-domain": "תחום:",
"passwordreset-email": "כתובת דוא\"ל:",
"passwordreset-emailtitle": "פרטי חשבון ב{{grammar:תחילית|{{SITENAME}}}}",
- "passwordreset-emailtext-ip": "מישהו (ככל הנראה אתם, מכתובת ה־IP מספר $1) ביקש איפוס של\nהסיסמה שלכם ב{{grammar:תחילית|{{SITENAME}}}} ($4). {{PLURAL:$3|חשבון המשתמש הבא שייך|חשבונות המשתמש הבאים שייכים}}\nלכתובת הדואר האלקטרוני הזאת:\n\n$2\n\n{{PLURAL:$3|סיסמה זמנית זו תפקע|סיסמאות זמניות אלה יפקעו}} תוך {{PLURAL:$5|יום|יומיים|$5 ימים}}.\nעליכם להיכנס ולבחור סיסמה חדשה עכשיו. אם מישהו אחר ביצע בקשה זו, או שנזכרתם בסיסמתכם\nהמקורית ואינכם רוצים עוד לשנות אותה, באפשרותכם להתעלם מהודעה זו ולהמשיך להשתמש בסיסמה\nהישנה.",
- "passwordreset-emailtext-user": "{{GENDER:$1|המשתמש|המשתמשת}} $1 ב{{GRAMMAR:תחילית|{{SITENAME}}}} {{GENDER:$1|ביקש|ביקשה}} איפוס של הסיסמה שלכם ב{{GRAMMAR:תחילית|{{SITENAME}}}}\n($4). {{PLURAL:$3|חשבון המשתמש הבא שייך|חשבונות המשתמש הבאים שייכים}} לכתובת הדואר האלקטרוני הזאת:\n\n$2\n\n{{PLURAL:$3|סיסמה זמנית זו תפקע|סיסמאות זמניות אלה יפקעו}} תוך {{PLURAL:$5|יום|יומיים|$5 ימים}}.\nעליכם להיכנס ולבחור סיסמה חדשה עכשיו. אם מישהו אחר ביצע בקשה זו, או שנזכרתם בסיסמתכם\nהמקורית ואינכם רוצים עוד לשנות אותה, באפשרותכם להתעלם מהודעה זו ולהמשיך להשתמש בסיסמה\nהישנה.",
+ "passwordreset-emailtext-ip": "מישהו (ככל הנראה אתם, מכתובת ה־IP מספר $1) ביקש איפוס של\nהסיסמה שלכם ב{{grammar:תחילית|{{SITENAME}}}}‏ ($4). {{PLURAL:$3|חשבון המשתמש הבא שייך|חשבונות המשתמש הבאים שייכים}}\nלכתובת הדואר האלקטרוני הזאת:\n\n$2\n\n{{PLURAL:$3|סיסמה זמנית זו תפקע|סיסמאות זמניות אלה יפקעו}} תוך {{PLURAL:$5|יום|יומיים|$5 ימים}}.\nעליכם להיכנס ולבחור סיסמה חדשה עכשיו. אם מישהו אחר ביצע בקשה זו, או אם נזכרתם בסיסמתכם\nהמקורית ואינכם רוצים עוד לשנות אותה, באפשרותכם להתעלם מהודעה זו ולהמשיך להשתמש בסיסמה\nהישנה.",
+ "passwordreset-emailtext-user": "המשתמש $1 ב{{GRAMMAR:תחילית|{{SITENAME}}}} ביקש איפוס של הסיסמה שלכם ב{{GRAMMAR:תחילית|{{SITENAME}}}}‏\n($4). {{PLURAL:$3|חשבון המשתמש הבא שייך|חשבונות המשתמש הבאים שייכים}} לכתובת הדואר האלקטרוני הזאת:\n\n$2\n\n{{PLURAL:$3|סיסמה זמנית זו תפקע|סיסמאות זמניות אלה יפקעו}} תוך {{PLURAL:$5|יום|יומיים|$5 ימים}}.\nעליכם להיכנס ולבחור סיסמה חדשה עכשיו. אם מישהו אחר ביצע בקשה זו, או אם נזכרתם בסיסמתכם\nהמקורית ואינכם רוצים עוד לשנות אותה, באפשרותכם להתעלם מהודעה זו ולהמשיך להשתמש בסיסמה\nהישנה.",
"passwordreset-emailelement": "שם משתמש:\n$1\n\nסיסמה זמנית:\n$2",
"passwordreset-emailsentemail": "אם כתובת הדואר האלקטרוני הזאת משויכת לחשבון שלך, אז יישלח דואר אלקטרוני לאיפוס הסיסמה.",
"passwordreset-emailsentusername": "אם יש כתובת דואר אלקטרוני שמשויכת לשם המשתמש הזה, אז יישלח דואר אלקטרוני לאיפוס הסיסמה.",
"changeemail-no-info": "נדרשת כניסה לחשבון כדי לגשת לדף זה ישירות.",
"changeemail-oldemail": "כתובת דוא\"ל נוכחית:",
"changeemail-newemail": "כתובת דוא\"ל חדשה:",
- "changeemail-newemail-help": "×¢×\9c×\99×\9b×\9d ×\9c×\94ש×\90×\99ר ש×\93×\94 ×\96×\94 ר×\99ק ×\90×\9d ×\91רצ×\95× ×\9b×\9d ×\9c×\94ס×\99ר ×\90ת ×\9bת×\95×\91ת ×\94×\93×\95×\90ר ×\94×\90×\9cק×\98ר×\95× ×\99 ש×\9c×\9b×\9d. ×\90×\9d תס×\99ר×\95 ×\90×\95ת×\94, ×\9c×\90 ת×\95×\9b×\9c×\95 ×\9c×\90פס ס×\99ס×\9e×\94 שש×\9b×\97ת×\9d ×\95×\9c×\90 ת×\95×\9b×\9c×\95 לקבל הודעות דואר אלקטרוני מאתר הוויקי הזה.",
+ "changeemail-newemail-help": "×\91×\90פשר×\95ת×\9a ×\9c×\94ש×\90×\99ר ש×\93×\94 ×\96×\94 ר×\99ק ×\90×\9d ×\91רצ×\95× ×\9a ×\9c×\94ס×\99ר ×\90ת ×\9bת×\95×\91ת ×\94×\93×\95×\90ר ×\94×\90×\9cק×\98ר×\95× ×\99 ש×\9c×\9a. ×\90×\9d ×\94×\99×\90 ת×\95סר, ×\9c×\90 ×\99×\94×\99×\94 ×\91×\90פשר×\95ת×\9a ×\9c×\90פס ס×\99ס×\9e×\94 שש×\9b×\97ת ×\95×\9c×\90 {{GENDER:|ת×\95×\9b×\9c|ת×\95×\9b×\9c×\99}} לקבל הודעות דואר אלקטרוני מאתר הוויקי הזה.",
"changeemail-none": "(אין)",
"changeemail-password": "סיסמה ב{{grammar:תחילית|{{SITENAME}}}}:",
"changeemail-submit": "שינוי כתובת הדוא\"ל",
"userjspreview": "'''Denkt drun datt Dir Äre Javascript nëmmen test.'''\n'''En ass nach net gespäichert!'''",
"sitecsspreview": "'''Denkt drun datt Dir dësen CSS just kuckt.\nE gouf nach net gespäichert!'''",
"sitejspreview": "'''Denkt drun datt Dir dëse JavaScript-Code just kuckt.\nE gouf nach net gespäichert!'''",
- "userinvalidconfigtitle": "'''Opgepasst:''' Et gëtt keen Ausgesinn (skin) \"$1\".\nDenkt drun datt eegen .css an .js Säiten e kleng geschriwwenen Titel benotzen, z. Bsp. {{ns:user}}:Foo/vector.css am Géigesaz zu {{ns:user}}:Foo/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Opgepasst:</strong> Et gëtt keen Ausgesinn (skin) \"$1\".\nDenkt drun datt personaliséiert .css .json an .js Säiten e kleng geschriwwenen Titel benotzen, z. Bsp. {{ns:user}}:Foo/vector.css am Géigesaz zu {{ns:user}}:Foo/Vector.css.",
"updated": "(Geännert)",
"note": "'''Notiz:'''",
"previewnote": "'''Denkt drun datt dëst nëmmen eng net gespäichert Versioun ass.'''\nÄr Ännerunge sinn nach net gespäichert!",
"default": "Standard",
"prefs-files": "Fichieren",
"prefs-custom-css": "Benotzerdefinéierten CSS",
+ "prefs-custom-json": "Personaliséierten JSON",
"prefs-custom-js": "Benotzerdefinéierte JS",
- "prefs-common-config": "Gemeinsam CSS/JS fir all Ausgesinn (skins):",
+ "prefs-common-config": "Gemeinsam CSS/JSON/JavaScript fir all Ausgesinn (skins):",
"prefs-reset-intro": "Dir kënnt dës Säit benotze fir Är Astellungen zréck op d'Standard-Astllungen ze setzen.\nDëst kann net réckgängeg gemaach ginn.",
"prefs-emailconfirm-label": "E-Mail Confirmatioun:",
"youremail": "E-Mail-Adress:",
"grant-createaccount": "Benotzerkonten opmaachen",
"grant-createeditmovepage": "Säiten uleeën, änneren a réckelen",
"grant-delete": "Säiten, Versiounen a Rubriken a Logbicher läschen",
- "grant-editinterface": "MediaWiki-Nummraum a Benotzer CSS/JavaScript änneren",
- "grant-editmycssjs": "Äre Benotzer CSS/JavaScript änneren",
+ "grant-editinterface": "MediaWiki-Nummraum a Benotzer CSS/JSON/JavaScript änneren",
+ "grant-editmycssjs": "Äre Benotzer CSS/JSON/JavaScript änneren",
"grant-editmyoptions": "Ännert Är Benotzerastellungen",
"grant-editmywatchlist": "Ännert Är Iwwerwaachungslëscht",
"grant-editpage": "Säiten déi et gëtt änneren",
"cascadeprotected": "Deze pagina kan niet bewerkt worden, omdat ze is opgenomen in de volgende {{PLURAL:$1|pagina|pagina's}} die beveiligd {{PLURAL:$1|is|zijn}} met de cascade-optie:\n$2",
"namespaceprotected": "U hebt geen rechten om pagina's in de naamruimte <strong>$1</strong> te bewerken.",
"customcssprotected": "U kunt deze CSS-pagina niet bewerken, omdat die persoonlijke instellingen van een andere gebruiker bevat.",
+ "customjsonprotected": "U kunt deze JSONpagina niet bewerken, omdat die persoonlijke instellingen van een andere gebruiker bevat.",
"customjsprotected": "U kunt deze JavaScriptpagina niet bewerken, omdat die persoonlijke instellingen van een andere gebruiker bevat.",
"mycustomcssprotected": "U hebt geen rechten om deze CSS-pagina te bewerken.",
+ "mycustomjsonprotected": "U hebt geen rechten om deze JSONpagina te bewerken.",
"mycustomjsprotected": "U hebt geen rechten om deze JavaScriptpagina te bewerken.",
"myprivateinfoprotected": "U hebt geen rechten om uw privégegevens te bewerken.",
"mypreferencesprotected": "U hebt geen rechten om uw voorkeuren aan te passen.",
"savechanges": "Wijzigingen opslaan",
"publishpage": "Pagina publiceren",
"publishchanges": "Wijzigingen publiceren",
+ "savearticle-start": "Pagina opslaan...",
+ "savechanges-start": "Wijzigingen opslaan...",
+ "publishpage-start": "Pagina publiceren...",
+ "publishchanges-start": "Wijzigingen publiceren...",
"preview": "Voorvertoning",
"showpreview": "Bewerking ter controle bekijken",
"showdiff": "Wijzigingen bekijken",
"blocked-notice-logextract": "Deze gebruiker is momenteel geblokkeerd.\nDe laatste regel uit het blokkeerlogboek wordt hieronder ter referentie weergegeven:",
"clearyourcache": "<strong>Opmerking:</strong> nadat u de wijzigingen hebt opgeslagen is het wellicht nodig uw browsercache te legen.\n* <strong>Firefox / Safari:</strong> houd <em>Shift</em> ingedrukt terwijl u op <em>Vernieuwen</em> klikt of druk op <em>Ctrl-F5</em> of <em>Ctrl-R</em> (<em>⌘-Shift-R</em> op een Mac)\n* <strong>Google Chrome:</strong> druk op <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> op een Mac)\n* <strong>Internet Explorer:</strong> houd <em>Ctrl</em> ingedrukt terwijl u op <em>Vernieuwen</em> klikt of druk op <em>Ctrl-F5</em>\n* '''Opera:''' ga naar <em>Menu → Instellingen</em> (<em>Opera → Voorkeuren</em> op een Mac) en daarna naar <em>Privacy & beveiliging → Browsegegevens wissen... → Tijdelijk opgeslgen afbeeldingen en bestanden</em>.",
"usercssyoucanpreview": "'''Tip:''' gebruik de knop \"{{int:showpreview}}\" om uw nieuwe CSS te testen alvorens op te slaan.",
+ "userjsonyoucanpreview": "'''Tip:''' gebruik de knop \"{{int:showpreview}}\" om uw nieuwe JSON te testen alvorens op te slaan.",
"userjsyoucanpreview": "'''Tip:''' gebruik de knop \"{{int:showpreview}}\" om uw nieuwe JavaScript te testen alvorens op te slaan.",
"usercsspreview": "'''Dit is alleen een voorvertoning van uw persoonlijke CSS.'''\n'''Deze is nog niet opgeslagen!'''",
+ "userjsonpreview": "<strong>Let op: u test nu uw persoonlijke JSON.\nDe pagina is niet opgeslagen!</strong>",
"userjspreview": "'''Let op: u test nu uw persoonlijke JavaScript.'''\n'''De pagina is niet opgeslagen!'''",
"sitecsspreview": "'''Dit is alleen een voorvertoning van de CSS.'''\n'''Deze is nog niet opgeslagen!'''",
+ "sitejsonpreview": "<strong>Dit is alleen een voorvertoning van de JSON configuratie.\nDeze is nog niet opgeslagen!</strong>",
"sitejspreview": "'''Dit is alleen een voorvertoning van de JavaScriptcode.'''\n'''Deze is nog niet opgeslagen!'''",
- "userinvalidconfigtitle": "'''Waarschuwing:''' er is geen uiterlijk \"$1\".\nUw eigen .css- en .js-pagina's beginnen met een kleine letter, bijvoorbeeld {{ns:user}}:Naam/vector.css in plaats van {{ns:user}}:Naam/Vector.css.",
+ "userinvalidconfigtitle": "<strong>Waarschuwing:</strong>''' er is geen vormgeving \"$1\".\nUw eigen .css-, .json- en .js-pagina's beginnen met een kleine letter, bijvoorbeeld {{ns:user}}:Naam/vector.css in plaats van {{ns:user}}:Naam/Vector.css.",
"updated": "(Bijgewerkt)",
"note": "<strong>Opmerking:</strong>",
"previewnote": "'''Let op: dit is een controlepagina.'''\nUw tekst is niet opgeslagen!",
"default": "standaard",
"prefs-files": "Bestanden",
"prefs-custom-css": "Aangepaste CSS",
- "prefs-custom-js": "Aangepast JavaScript",
- "prefs-common-config": "Gedeelde CSS/JavaScript voor elke vormgeving:",
+ "prefs-custom-json": "Aangepaste JSON",
+ "prefs-custom-js": "Aangepaste JavaScript",
+ "prefs-common-config": "Gedeelde CSS/JSON/JavaScript voor elke vormgeving:",
"prefs-reset-intro": "Gebruik deze functie om uw voorkeuren te herstellen naar de standaardinstellingen.\nDeze handeling kan niet ongedaan gemaakt worden.",
"prefs-emailconfirm-label": "E-mailbevestiging:",
"youremail": "E-mailadres:",
"right-editcontentmodel": "Het paginainhoudmodel bewerken",
"right-editinterface": "De gebruikersinterface bewerken",
"right-editusercss": "De CSS-bestanden van andere gebruikers bewerken",
+ "right-edituserjson": "De JSONbestanden van andere gebruikers bewerken",
"right-edituserjs": "De JavaScriptbestanden van andere gebruikers bewerken",
"right-editmyusercss": "Uw eigen CSS-pagina's bewerken",
+ "right-editmyuserjson": "Uw eigen JSonpagina's bewerken",
"right-editmyuserjs": "Uw eigen JavaScriptpagina's bewerken",
"right-viewmywatchlist": "Uw eigen volglijst bekijken",
"right-editmywatchlist": "Uw eigen volglijst bewerken. Via sommige handelingen kunnen nog steeds pagina's toegevoegd worden, zelfs zonder deze bevoegdheid",
"grant-createaccount": "Accounts aanmaken",
"grant-createeditmovepage": "Pagina's aanmaken, bewerken en hernoemen",
"grant-delete": "Pagina's, wijzigingen en logboekregels verwijderen",
- "grant-editinterface": "De naamruimte MediaWiki en CSS en JavaScript van gebruikers bewerken",
- "grant-editmycssjs": "Eigen CSS en JavaScript bewerken",
+ "grant-editinterface": "De naamruimte MediaWiki en CSS, JSON en JavaScript van gebruikers bewerken",
+ "grant-editmycssjs": "Eigen CSS, JSON en JavaScript bewerken",
"grant-editmyoptions": "Eigen voorkeuren instellen",
"grant-editmywatchlist": "Eigen volglijst bewerken",
"grant-editpage": "Bestaande pagina's bewerken",
"group-bot.css": "/* CSS die hier wordt geplaatst heeft alleen invloed op robots */",
"group-sysop.css": "/* CSS die hier wordt geplaatst heeft alleen invloed op beheerders */",
"group-bureaucrat.css": "/* CSS die hier wordt geplaatst heeft alleen invloed op bureaucraten */",
+ "common.json": "/* JSON die hier wordt geplaatst heeft invloed op alle pagina's voor alle gebruikers */",
"common.js": "/* JavaScript die hier wordt geplaatst heeft invloed op alle pagina's voor alle gebruikers */",
"group-autoconfirmed.js": "/* JavaScript die hier wordt geplaatst heeft alleen invloed op automatisch bevestigde gebruikers */",
"group-user.js": "/* JavaScript die hier wordt geplaatst heeft alleen invloed op geregistreerde gebruikers */",
"unlinkaccounts-success": "Het account is ontkoppeld.",
"authenticationdatachange-ignored": "De wijziging van de authenticatiegegevens is niet afgehandeld. Misschien is er geen provider geconfigureerd?",
"userjsispublic": "Let op: JavaScript deelpagina's moeten geen vertrouwelijke gegevens bevatten omdat ze kunnen worden bekeken door andere gebruikers.",
+ "userjsonispublic": "Let op: JSON deelpagina's moeten geen vertrouwelijke gegevens bevatten omdat ze kunnen worden bekeken door andere gebruikers.",
"usercssispublic": "Let op: CSS deelpagina's moeten geen vertrouwelijke gegevens bevatten omdat ze kunnen worden bekeken door andere gebruikers.",
"restrictionsfield-badip": "Ongeldig IP-adres of range: $1",
"restrictionsfield-label": "Toegestane IP-ranges:",
"BarbaraAckles",
"Trigonometria87",
"RadiX",
- "Fitoschido"
+ "Fitoschido",
+ "Ed g2s"
]
},
"tog-underline": "Ligação sublinhada:",
"savechanges": "Salvar alterações",
"publishpage": "Publicar página",
"publishchanges": "Publicar alterações",
- "savearticle-start": "Salvar página ...",
+ "savearticle-start": "Salvar página…",
"savechanges-start": "Salvar alterações…",
- "publishpage-start": "Publicar página ...",
- "publishchanges-start": "Publicar alterações...",
+ "publishpage-start": "Publicar página…",
+ "publishchanges-start": "Publicar alterações…",
"preview": "Pré-visualização",
"showpreview": "Mostrar previsão",
"showdiff": "Mostrar alterações",
"cascadeprotected": "Данная страница защищена от изменений, поскольку она включена в {{PLURAL:$1|1=следующую страницу, для которой|следующие страницы, для которых}} включена каскадная защита:\n$2",
"namespaceprotected": "У вас нет разрешения редактировать страницы в пространстве имён «$1».",
"customcssprotected": "У вас нет разрешения редактировать эту CSS-страницу, так как она содержит личные настройки другого участника.",
+ "customjsonprotected": "У вас нет разрешения редактировать эту JSON-страницу, так как она содержит личные настройки другого участника.",
"customjsprotected": "У вас нет разрешения редактировать эту JavaScript-страницу, так как она содержит личные настройки другого участника.",
"mycustomcssprotected": "У вас нет прав для редактирования этого CSS страницы.",
+ "mycustomjsonprotected": "У вас нет прав для редактирования этой JSON-страницы.",
"mycustomjsprotected": "У вас нет прав для редактирования JavaScript на странице.",
"myprivateinfoprotected": "У вас нет разрешения на изменение вашей личной информации",
"mypreferencesprotected": "У вас нет прав для редактирования настроек.",
"wrongpasswordempty": "Пожалуйста, введите непустой пароль.",
"passwordtooshort": "Пароль должен состоять не менее, чем из $1 {{PLURAL:$1|символа|символов}}.",
"passwordtoolong": "Пароль не может содержать более {{PLURAL:$1|1=$1 символа|$1 символов}}.",
- "passwordtoopopular": "ЧаÑ\81Ñ\82о вÑ\8bбиÑ\80аемÑ\8bе паÑ\80оли не могÑ\83Ñ\82 бÑ\8bÑ\82Ñ\8c иÑ\81полÑ\8cзованÑ\8b. Ð\9fожалÑ\83йÑ\81Ñ\82а, вÑ\8bбеÑ\80иÑ\82е более Ñ\83никалÑ\8cнÑ\8bй паÑ\80оль.",
+ "passwordtoopopular": "ЧаÑ\81Ñ\82о вÑ\8bбиÑ\80аемÑ\8bе паÑ\80оли не могÑ\83Ñ\82 бÑ\8bÑ\82Ñ\8c иÑ\81полÑ\8cзованÑ\8b. Ð\9fожалÑ\83йÑ\81Ñ\82а, вÑ\8bбеÑ\80иÑ\82е паÑ\80олÑ\8c, коÑ\82оÑ\80Ñ\8bй Ñ\81ложнее Ñ\83гадаÑ\82ь.",
"password-name-match": "Введённый пароль должен отличаться от имени участника.",
"password-login-forbidden": "Использование этого имени участника и пароля запрещено.",
"mailmypassword": "Сбросить пароль",
"savechanges": "Записать страницу",
"publishpage": "Создать страницу",
"publishchanges": "Записать страницу",
+ "savearticle-start": "Сохранить страницу…",
+ "savechanges-start": "Сохранить изменения…",
+ "publishpage-start": "Опубликовать страницу…",
+ "publishchanges-start": "Опубликовать изменения…",
"preview": "Предпросмотр",
"showpreview": "Предварительный просмотр",
"showdiff": "Внесённые изменения",
"blocked-notice-logextract": "{{GENDER:$1|Этот участник|Эта участница}} в данный момент {{GENDER:$1|заблокирован|заблокирована}}.\nНиже приведена последняя запись из журнала блокировок:",
"clearyourcache": "<strong>Замечание.</strong> Возможно, после сохранения вам придётся очистить кэш своего браузера, чтобы увидеть изменения.\n* <strong>Firefox / Safari:</strong> Удерживая клавишу <em>Shift</em>, нажмите на панели инструментов <em>Обновить</em> либо нажмите <em>Ctrl-F5</em> или <em>Ctrl-R</em> (<em>⌘-R</em> на Mac)\n* <strong>Google Chrome:</strong> Нажмите <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> на Mac)\n* <strong>Internet Explorer:</strong> Удерживая <em>Ctrl</em>, нажмите <em>Обновить</em> либо нажмите <em>Ctrl-F5</em>\n* <strong>Opera:</strong> Перейдите в <em>Menu → Настройки</em> (<em>Opera → Настройки</em> на Mac), а затем <em>Безопасность → Очистить историю посещений → Кэшированные изображения и файлы</em>",
"usercssyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый CSS-файл перед сохранением.",
+ "userjsonyoucanpreview": "<strong>Подсказка:</strong> Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый JSON-файл перед сохранением.",
"userjsyoucanpreview": "'''Подсказка.''' Нажмите кнопку «{{int:showpreview}}», чтобы проверить свой новый JS-файл перед сохранением.",
"usercsspreview": "'''Помните, что это только предварительный просмотр вашего CSS-файла, он ещё не сохранён!'''",
+ "userjsonpreview": "<strong>Помните, что это только тестовый/предварительный просмотр вашей JSON-конфигурации.\nОна ещё не сохранена!</strong>",
"userjspreview": "'''Помните, что это только предварительный просмотр вашего javascript-файла, он ещё не сохранён!'''",
"sitecsspreview": "'''Помните, что вы только предварительно просматриваете этот CSS.'''\n'''Он ещё не сохранён!'''",
+ "sitejsonpreview": "<strong>Помните, что это только предварительный просмотр JSON-конфигурации.\nОна ещё не сохранена!</strong>",
"sitejspreview": "'''Помните, что вы только предварительно просматриваете этот JavaScript-код.'''\n'''Он ещё не сохранён!'''",
- "userinvalidconfigtitle": "'''Внимание:''' тема оформления «$1» не найдена. Помните, что пользовательские страницы .css и .js должны иметь название, состоящее только из строчных букв, например «{{ns:user}}:Некто/vector.css», а не «{{ns:user}}:Некто/Vector.css».",
+ "userinvalidconfigtitle": "<strong>Внимание:</strong> тема оформления «$1» не найдена. Пользовательские страницы .css, .json и .js должны иметь название, состоящее только из строчных букв, например «{{ns:user}}:Некто/vector.css», а не «{{ns:user}}:Некто/Vector.css».",
"updated": "(Обновлена)",
"note": "'''Примечание:'''",
"previewnote": "'''Помните, что это только предварительный просмотр.'''\nВаши изменения ещё не были сохранены!",
"default": "по умолчанию",
"prefs-files": "Файлы",
"prefs-custom-css": "Собственный CSS",
+ "prefs-custom-json": "Пользовательский JSON",
"prefs-custom-js": "Собственный JS",
- "prefs-common-config": "Общие CSS/JS для всех тем оформления:",
+ "prefs-common-config": "Общие CSS/JSON/JavaScript для всех тем оформления:",
"prefs-reset-intro": "Эта страница может быть использована для сброса ваших настроек на стандартные.\nУчтите, что это действие невозможно отменить.",
"prefs-emailconfirm-label": "Подтверждение электронной почты:",
"youremail": "Электронная почта:",
"right-editcontentmodel": "редактирование контентной модели страницы",
"right-editinterface": "изменение пользовательского интерфейса",
"right-editusercss": "правка CSS-файлов других участников",
+ "right-edituserjson": "правка JSON-файлов других участников",
"right-edituserjs": "правка JavaScript-файлов других участников",
"right-editmyusercss": "редактирование своих пользовательских CSS-файлов",
+ "right-editmyuserjson": "редактирование своих пользовательских JSON-файлов",
"right-editmyuserjs": "редактирование своих пользовательских JavaScript-файлов",
"right-viewmywatchlist": "просмотр своего списка наблюдения",
"right-editmywatchlist": "редактирование своего списка наблюдения",
"grant-createaccount": "Создание учётных записей",
"grant-createeditmovepage": "Создание, редактирование и переименование страниц",
"grant-delete": "Удаление страниц, правок и записей журнала",
- "grant-editinterface": "Правка пространства имён MediaWiki и пользовательских CSS/JavaScript",
- "grant-editmycssjs": "РедакÑ\82иÑ\80ование ваÑ\88иÑ\85 полÑ\8cзоваÑ\82елÑ\8cÑ\81киÑ\85 CSS/JavaScript",
+ "grant-editinterface": "Правка пространства имён MediaWiki и пользовательских CSS/JSON/JavaScript",
+ "grant-editmycssjs": "Ð\9fÑ\80авка ваÑ\88иÑ\85 полÑ\8cзоваÑ\82елÑ\8cÑ\81киÑ\85 CSS/JSON/JavaScript",
"grant-editmyoptions": "Редактирование ваших персональных настроек",
"grant-editmywatchlist": "Редактирование вашего списка наблюдения",
"grant-editpage": "Редактирование существующих страниц",
"group-bot.css": "/* Размещённый здесь CSS будет применяться только для ботов */",
"group-sysop.css": "/* Размещённый здесь CSS будет применяться только для администраторов */",
"group-bureaucrat.css": "/* Размещённый здесь CSS будет применяться только для бюрократов */",
+ "common.json": "/* Размещённый здесь JSON-код будет загружаться всем участникам при каждом обращении к странице */",
"common.js": "/* Размещённый здесь код JavaScript будет загружаться пользователям при обращении к каждой странице */",
"group-autoconfirmed.js": "/* Размещённый здесь код JavaScript будет загружаться только участникам, имеющим статус автоподтверждённых (autoconfirmed) */",
"group-user.js": "/* Размещённый здесь JavaScript будет применяться только для зарегистрированных пользователей */",
"unlinkaccounts-success": "Учетная запись была отвязан.",
"authenticationdatachange-ignored": "Изменение данных для проверки подлинности не было обработано. Может быть, не был настроен ни один провайдер?",
"userjsispublic": "Обратите внимание: подстраницы JavaScript не должны содержать конфиденциальные сведения, поскольку они доступны для просмотра другим участникам.",
+ "userjsonispublic": "Обратите внимание: подстраницы JSON не должны содержать конфиденциальных данных, поскольку они доступны для просмотра другими участниками.",
"usercssispublic": "Обратите внимание: подстраницы CSS не должны содержать конфиденциальные сведения, поскольку они доступны для просмотра другим участникам.",
"restrictionsfield-badip": "Недопустимый IP-адрес или диапазон адресов: $1",
"restrictionsfield-label": "Разрешённые диапазоны IP-адресов:",
--- /dev/null
+--
+-- patch-image-img_description_id.sql
+--
+-- T188132. Add `img_description_id` to the `image` table.
+
+ALTER TABLE /*_*/image
+ ADD COLUMN img_description_id bigint unsigned NOT NULL DEFAULT 0 AFTER img_description;
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Remove autopatrol logs in the logging table.
+ *
+ * @ingroup Maintenance
+ */
+class DeleteAutoPatrolLogs extends Maintenance {
+
+ public function __construct() {
+ parent::__construct();
+ $this->addDescription( 'Remove autopatrol logs in the logging table' );
+ $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' );
+ $this->addOption(
+ 'check-old',
+ 'Check old patrol logs (for deleting old format autopatrols).' .
+ 'Note that this will not delete rows older than 2011 (MediaWiki 1.18).'
+ );
+ $this->addOption(
+ 'before',
+ 'Timestamp to delete only before that time, all MediaWiki timestamp formats are accepted',
+ false,
+ true
+ );
+ $this->addOption(
+ 'from-id',
+ 'First row (log id) to start updating from',
+ false,
+ true
+ );
+ $this->addOption(
+ 'sleep',
+ 'Sleep time (in seconds) between every batch',
+ false,
+ true
+ );
+ $this->setBatchSize( 1000 );
+ }
+
+ public function execute() {
+ $this->setBatchSize( $this->getOption( 'batch-size', $this->getBatchSize() ) );
+
+ $sleep = (int)$this->getOption( 'sleep', 10 );
+ $fromId = $this->getOption( 'from-id', null );
+ $this->countDown( 5 );
+ while ( true ) {
+ if ( $this->hasOption( 'check-old' ) ) {
+ $rowsData = $this->getRowsOld( $fromId );
+ // We reached end of the table
+ if ( !$rowsData ) {
+ break;
+ }
+ $rows = $rowsData['rows'];
+ $fromId = $rowsData['lastId'];
+
+ // There is nothing to delete in this batch
+ if ( !$rows ) {
+ continue;
+ }
+ } else {
+ $rows = $this->getRows( $fromId );
+ if ( !$rows ) {
+ break;
+ }
+ $fromId = end( $rows );
+ }
+
+ if ( $this->hasOption( 'dry-run' ) ) {
+ $this->output( 'These rows will get deleted: ' . implode( ', ', $rows ) . "\n" );
+ } else {
+ $this->deleteRows( $rows );
+ $this->output( 'Processed up to row id ' . end( $rows ) . "\n" );
+ }
+
+ if ( $sleep > 0 ) {
+ sleep( $sleep );
+ }
+ }
+ }
+
+ private function getRows( $fromId ) {
+ $dbr = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection(
+ DB_REPLICA
+ );
+ $before = $this->getOption( 'before', false );
+
+ $conds = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ ];
+
+ if ( $fromId ) {
+ $conds[] = 'log_id > ' . $dbr->addQuotes( $fromId );
+ }
+
+ if ( $before ) {
+ $conds[] = 'log_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $before ) );
+ }
+
+ return $dbr->selectFieldValues(
+ 'logging',
+ 'log_id',
+ $conds,
+ __METHOD__,
+ [ 'LIMIT' => $this->getBatchSize() ]
+ );
+ }
+
+ private function getRowsOld( $fromId ) {
+ $dbr = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection(
+ DB_REPLICA
+ );
+ $batchSize = $this->getBatchSize();
+ $before = $this->getOption( 'before', false );
+
+ $conds = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ ];
+
+ if ( $fromId ) {
+ $conds[] = 'log_id > ' . $dbr->addQuotes( $fromId );
+ }
+
+ if ( $before ) {
+ $conds[] = 'log_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $before ) );
+ }
+
+ $result = $dbr->select(
+ 'logging',
+ [ 'log_id', 'log_params' ],
+ $conds,
+ __METHOD__,
+ [ 'LIMIT' => $batchSize ]
+ );
+
+ $last = null;
+ $autopatrolls = [];
+ foreach ( $result as $row ) {
+ $last = $row->log_id;
+ Wikimedia\suppressWarnings();
+ $params = unserialize( $row->log_params );
+ Wikimedia\restoreWarnings();
+
+ // Skipping really old rows, before 2011
+ if ( !is_array( $params ) || !array_key_exists( '6::auto', $params ) ) {
+ continue;
+ }
+
+ $auto = $params['6::auto'];
+ if ( $auto ) {
+ $autopatrolls[] = $row->log_id;
+ }
+ }
+
+ if ( $last === null ) {
+ return null;
+ }
+
+ return [ 'rows' => $autopatrolls, 'lastId' => $last ];
+ }
+
+ private function deleteRows( array $rows ) {
+ $dbw = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection(
+ DB_MASTER
+ );
+
+ $dbw->delete(
+ 'logging',
+ [ 'log_id' => $rows ],
+ __METHOD__
+ );
+
+ MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->waitForReplication();
+ }
+
+}
+
+$maintClass = DeleteAutoPatrolLogs::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
*
* @since 1.25
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Daniel Kinzler
*/
class ExportSites extends Maintenance {
--- /dev/null
+--
+-- patch-image-img_description_id.sql
+--
+-- T188132. Add `img_description_id` to the `image` table.
+
+ALTER TABLE /*_*/image ADD img_description_id bigint NOT NULL CONSTRAINT DF_img_description_id DEFAULT 0 CONSTRAINT FK_img_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id);
-- Description field as entered by the uploader.
-- This is displayed in image upload history and logs.
img_description nvarchar(255) NOT NULL CONSTRAINT DF_img_description DEFAULT '',
+ img_description_id bigint NOT NULL CONSTRAINT DF_img_description_id DEFAULT 0 CONSTRAINT FK_img_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
-- user_id and user_name of uploader.
img_user int REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
--- /dev/null
+--
+-- patch-image-img_description_id.sql
+--
+-- T188132. Add `img_description_id` to the `image` table.
+
+ALTER TABLE &mw_prefix.image ADD ( img_description_id NUMBER DEFAULT 0 NOT NULL );
+ALTER TABLE &mw_prefix.image ADD CONSTRAINT &mw_prefix.oldimage_fk2 FOREIGN KEY (img_description_id) REFERENCES &mw_prefix."COMMENT"(comment_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
img_major_mime VARCHAR2(32) DEFAULT 'unknown',
img_minor_mime VARCHAR2(100) DEFAULT 'unknown',
img_description VARCHAR2(255),
+ img_description_id NUMBER DEFAULT 0 NOT NULL,
img_user NUMBER DEFAULT 0 NOT NULL,
img_user_text VARCHAR2(255) NULL,
img_actor NUMBER DEFAULT 0 NOT NULL,
);
ALTER TABLE &mw_prefix.image ADD CONSTRAINT &mw_prefix.image_pk PRIMARY KEY (img_name);
ALTER TABLE &mw_prefix.image ADD CONSTRAINT &mw_prefix.image_fk1 FOREIGN KEY (img_user) REFERENCES &mw_prefix.mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE &mw_prefix.image ADD CONSTRAINT &mw_prefix.image_fk2 FOREIGN KEY (img_description_id) REFERENCES &mw_prefix."COMMENT"(comment_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX &mw_prefix.image_i01 ON &mw_prefix.image (img_user_text,img_timestamp);
CREATE INDEX &mw_prefix.image_i02 ON &mw_prefix.image (img_size);
CREATE INDEX &mw_prefix.image_i03 ON &mw_prefix.image (img_timestamp);
img_major_mime TEXT DEFAULT 'unknown',
img_minor_mime TEXT DEFAULT 'unknown',
img_description TEXT NOT NULL DEFAULT '',
+ img_description_id INTEGER NOT NULL DEFAULT 0,
img_user INTEGER NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
img_user_text TEXT NOT NULL DEFAULT '',
img_actor INTEGER NOT NULL DEFAULT 0,
* @file
* @ingroup Maintenance
* @author Rob Church <robchur@gmail.com>
- * @licence GNU General Public Licence 2.0 or later
+ * @license GNU General Public Licence 2.0 or later
*/
use Wikimedia\Rdbms\IDatabase;
img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
img_minor_mime varbinary(100) NOT NULL default "unknown",
img_description varbinary(767) NOT NULL default '',
+ img_description_id bigint unsigned NOT NULL DEFAULT 0,
img_user int unsigned NOT NULL default 0,
img_user_text varchar(255) binary NOT NULL DEFAULT '',
img_actor bigint unsigned NOT NULL DEFAULT 0,
INSERT OR IGNORE INTO /*_*/image_tmp (
img_name, img_size, img_width, img_height, img_metadata, img_bits,
- img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
- img_user_text, img_timestamp, img_sha1)
+ img_media_type, img_major_mime, img_minor_mime, img_description,
+ img_description_id, img_user, img_user_text, img_timestamp, img_sha1)
SELECT
img_name, img_size, img_width, img_height, img_metadata, img_bits,
- img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
- img_user_text, img_timestamp, img_sha1
+ img_media_type, img_major_mime, img_minor_mime, img_description,
+ img_description_id, img_user, img_user_text, img_timestamp, img_sha1
FROM /*_*/image;
DROP TABLE /*_*/image;
--- /dev/null
+--
+-- patch-image-img_description_id.sql
+--
+-- T188132. Add `img_description_id` to the `image` table.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/image_tmp;
+CREATE TABLE /*_*/image_tmp (
+ img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+ img_size int unsigned NOT NULL default 0,
+ img_width int NOT NULL default 0,
+ img_height int NOT NULL default 0,
+ img_metadata mediumblob NOT NULL,
+ img_bits int NOT NULL default 0,
+ img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+ img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+ img_minor_mime varbinary(100) NOT NULL default "unknown",
+ img_description varbinary(767) NOT NULL default '',
+ img_description_id bigint unsigned NOT NULL DEFAULT 0,
+ img_user int unsigned NOT NULL default 0,
+ img_user_text varchar(255) binary NOT NULL default '',
+ img_timestamp varbinary(14) NOT NULL default '',
+ img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+
+INSERT OR IGNORE INTO /*_*/image_tmp (
+ img_name, img_size, img_width, img_height, img_metadata, img_bits,
+ img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+ img_user_text, img_timestamp, img_sha1)
+ SELECT
+ img_name, img_size, img_width, img_height, img_metadata, img_bits,
+ img_media_type, img_major_mime, img_minor_mime, img_description, img_user,
+ img_user_text, img_timestamp, img_sha1
+ FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+COMMIT;
-- Description field as entered by the uploader.
-- This is displayed in image upload history and logs.
- -- Deprecated in favor of image_comment_temp.imgcomment_description_id.
+ -- Deprecated in favor of img_description_id.
img_description varbinary(767) NOT NULL default '',
+ img_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that img_description should be used)
+
-- user_id and user_name of uploader.
-- Deprecated in favor of img_actor.
img_user int unsigned NOT NULL default 0,
<?php
/**
- * @large
+ * @group large
* @covers CurlHttpRequest
*/
class CurlHttpRequestTest extends MWHttpRequestTestCase {
<?php
/**
- * @large
+ * @group large
* @covers PhpHttpRequest
*/
class PhpHttpRequestTest extends MWHttpRequestTestCase {
* Meant to run on vagrant, although will probably work on other setups
* as long as firejail and sudo has similar config.
*
- * @large
+ * @group large
* @group Shell
* @covers FirejailCommand
*/
* @group Action
* @group Database
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Thiemo Kreuz
*/
class ActionTest extends MediaWikiTestCase {
use MediaWiki\MediaWikiServices;
/**
- * @medium
+ * @group medium
* @group API
* @group Database
*
}
/**
- * @medium
+ * @group medium
*/
public function testA() {
$this->mVerbose = false;
/**
* Test smart continue - list=allpages
- * @medium
+ * @group medium
*/
public function test1List() {
$this->mVerbose = false;
/**
* Test smart continue - list=allpages|alltransclusions
- * @medium
+ * @group medium
*/
public function test2Lists() {
$this->mVerbose = false;
/**
* Test smart continue - generator=allpages, prop=links
- * @medium
+ * @group medium
*/
public function testGen1Prop() {
$this->mVerbose = false;
/**
* Test smart continue - generator=allpages, prop=links|templates
- * @medium
+ * @group medium
*/
public function testGen2Prop() {
$this->mVerbose = false;
/**
* Test smart continue - generator=allpages, prop=links, list=alltransclusions
- * @medium
+ * @group medium
*/
public function testGen1Prop1List() {
$this->mVerbose = false;
/**
* Test smart continue - generator=allpages, prop=links|templates,
* list=alllinks|alltransclusions, meta=siteinfo
- * @medium
+ * @group medium
*/
public function testGen2Prop2List1Meta() {
$this->mVerbose = false;
/**
* Test smart continue - generator=templates, prop=templates
- * @medium
+ * @group medium
*/
public function testSameGenAndProp() {
$this->mVerbose = false;
/**
* Test smart continue - generator=allpages, list=allpages
- * @medium
+ * @group medium
*/
public function testSameGenList() {
$this->mVerbose = false;
/**
* @covers HTMLForm
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Gergő Tisza
* @author Thiemo Mättig
*/
* Integration test that checks import success and
* LinkCache integration.
*
- * @large
+ * @group large
* @group Database
* @covers ImportStreamSource
* @covers ImportReporter
*
* @group JobQueue
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Thiemo Kreuz
*/
class JobQueueMemoryTest extends PHPUnit\Framework\TestCase {
* @group JobQueue
* @group Database
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Addshore
*/
class CategoryMembershipChangeJobTest extends MediaWikiTestCase {
* @group JobQueue
* @group Database
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Addshore
*/
class ClearUserWatchlistJobTest extends MediaWikiTestCase {
* Consecutive calls to the memoized callable with the same arguments
* should result in just one invocation of the underlying callable.
*
- * @requires function apc_store/apcu_store
+ * @requires extension apcu
*/
public function testCallableMemoized() {
$observer = $this->getMockBuilder( stdClass::class )
$db->listViews( '' ) );
}
- /**
- * @covers Wikimedia\Rdbms\MySQLMasterPos
- */
public function testBinLogName() {
$pos = new MySQLMasterPos( "db1052.2424/4643", 1 );
- $this->assertEquals( "db1052", $pos->getLogName() );
+ $this->assertEquals( "db1052", $pos->binlog );
$this->assertEquals( "db1052.2424", $pos->getLogFile() );
- $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() );
+ $this->assertEquals( [ 2424, 4643 ], $pos->pos );
}
/**
],
// MySQL GTID style
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:23', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:24', $now ),
true,
false
],
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
true,
false
],
[
- new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
- new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
+ new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
+ new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
false,
false
],
],
[
new MySQLMasterPos(
- '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
- '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
- '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
+ '2E11FA47-71CA-11E1-9E33-C80AA9429562:5,' .
+ '3E11FA47-71CA-11E1-9E33-C80AA9429562:99,' .
+ '7E11FA47-71CA-11E1-9E33-C80AA9429562:30',
1
),
new MySQLMasterPos(
- '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
- '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
+ '1E11FA47-71CA-11E1-9E33-C80AA9429562:100,' .
+ '3E11FA47-71CA-11E1-9E33-C80AA9429562:66',
1
),
- [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ]
+ [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ]
]
];
}
];
}
- /**
- * @dataProvider provideGtidData
- * @covers Wikimedia\Rdbms\MySQLMasterPos
- * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos
- * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos
- */
- public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) {
- $db = $this->getMockBuilder( DatabaseMysqli::class )
- ->disableOriginalConstructor()
- ->setMethods( [
- 'useGTIDs',
- 'getServerGTIDs',
- 'getServerRoleStatus',
- 'getServerId',
- 'getServerUUID'
- ] )
- ->getMock();
-
- $db->method( 'useGTIDs' )->willReturn( true );
- $db->method( 'getServerGTIDs' )->willReturn( $gtable );
- $db->method( 'getServerRoleStatus' )->willReturnCallback(
- function ( $role ) use ( $rBLtable, $mBLtable ) {
- if ( $role === 'SLAVE' ) {
- return $rBLtable;
- } elseif ( $role === 'MASTER' ) {
- return $mBLtable;
- }
-
- return null;
- }
- );
- $db->method( 'getServerId' )->willReturn( 1 );
- $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' );
-
- if ( is_array( $rGTIDs ) ) {
- $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() );
- } else {
- $this->assertEquals( false, $db->getReplicaPos() );
- }
- if ( is_array( $mGTIDs ) ) {
- $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() );
- } else {
- $this->assertEquals( false, $db->getMasterPos() );
- }
- }
-
- public static function provideGtidData() {
- return [
- // MariaDB
- [
- [
- 'gtid_domain_id' => 100,
- 'gtid_current_pos' => '100-13-77',
- 'gtid_binlog_pos' => '100-13-77',
- 'gtid_slave_pos' => null // master
- ],
- [],
- [
- 'File' => 'host.1600',
- 'Pos' => '77'
- ],
- [ '100' => '100-13-77' ],
- [ '100' => '100-13-77' ]
- ],
- [
- [
- 'gtid_domain_id' => 100,
- 'gtid_current_pos' => '100-13-77',
- 'gtid_binlog_pos' => '100-13-77',
- 'gtid_slave_pos' => '100-13-77' // replica
- ],
- [
- 'Relay_Master_Log_File' => 'host.1600',
- 'Exec_Master_Log_Pos' => '77'
- ],
- [],
- [ '100' => '100-13-77' ],
- [ '100' => '100-13-77' ]
- ],
- [
- [
- 'gtid_current_pos' => '100-13-77',
- 'gtid_binlog_pos' => '100-13-77',
- 'gtid_slave_pos' => '100-13-77' // replica
- ],
- [
- 'Relay_Master_Log_File' => 'host.1600',
- 'Exec_Master_Log_Pos' => '77'
- ],
- [],
- [ '100' => '100-13-77' ],
- [ '100' => '100-13-77' ]
- ],
- // MySQL
- [
- [
- 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77'
- ],
- [
- 'Relay_Master_Log_File' => 'host.1600',
- 'Exec_Master_Log_Pos' => '77'
- ],
- [], // only a replica
- [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
- => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
- // replica/master use same var
- [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
- => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ],
- ],
- [
- [
- 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' .
- '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77'
- ],
- [
- 'Relay_Master_Log_File' => 'host.1600',
- 'Exec_Master_Log_Pos' => '77'
- ],
- [], // only a replica
- [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
- => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
- // replica/master use same var
- [ '2E11FA47-71CA-11E1-9E33-C80AA9429562'
- => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ],
- ],
- [
- [
- 'gtid_executed' => null // not enabled?
- ],
- [
- 'Relay_Master_Log_File' => 'host.1600',
- 'Exec_Master_Log_Pos' => '77'
- ],
- [], // only a replica
- [], // binlog fallback
- false
- ],
- [
- [
- 'gtid_executed' => null // not enabled?
- ],
- [], // no replication
- [], // no replication
- false,
- false
- ]
- ];
- }
-
/**
* @covers Wikimedia\Rdbms\MySQLMasterPos
*/
* Note: the following groups are not used by PHPUnit.
* The list in ParserTestFileSuite::__construct() is used instead.
*
- * @large
+ * @group large
* @group Database
* @group Parser
* @group ParserTests
<?php
/**
- * @medium
+ * @group medium
* @group Database
* @covers FormattedRCFeed
* @covers RecentChange
<?php
/**
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Addshore
*
* @covers SpecialBlankpage
*
* @since 1.26
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
* @author Jeroen De Dauw < jeroendedauw@gmail.com >
* @author Daniel Kinzler
* @author Addshore
*
* @since 1.30
*
- * @licence GNU GPL v2+
+ * @license GNU GPL v2+
*/
class SpecialShortpagesTest extends MediaWikiTestCase {
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Maintenance;
+
+use DeleteAutoPatrolLogs;
+
+/**
+ * @group Database
+ * @covers DeleteAutoPatrolLogs
+ */
+class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
+
+ public function getMaintenanceClass() {
+ return DeleteAutoPatrolLogs::class;
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $this->tablesUsed = [ 'logging' ];
+
+ $this->cleanLoggingTable();
+ $this->insertLoggingData();
+ }
+
+ private function cleanLoggingTable() {
+ wfGetDB( DB_MASTER )->delete( 'logging', '*' );
+ }
+
+ private function insertLoggingData() {
+ $logs = [];
+
+ // Manual patrolling
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => 7251,
+ 'log_params' => '',
+ 'log_timestamp' => 20041223210426
+ ];
+
+ // Autopatrol #1
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => 7252,
+ 'log_params' => '',
+ 'log_timestamp' => 20051223210426
+ ];
+
+ // Block
+ $logs[] = [
+ 'log_type' => 'block',
+ 'log_action' => 'block',
+ 'log_user' => 7253,
+ 'log_params' => '',
+ 'log_timestamp' => 20061223210426
+ ];
+
+ // Very old/ invalid patrol
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => 7253,
+ 'log_params' => 'nanana',
+ 'log_timestamp' => 20061223210426
+ ];
+
+ // Autopatrol #2
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => 7254,
+ 'log_params' => '',
+ 'log_timestamp' => 20071223210426
+ ];
+
+ // Autopatrol #3 old way
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => 7255,
+ 'log_params' => serialize( [ '6::auto' => true ] ),
+ 'log_timestamp' => 20081223210426
+ ];
+
+ // Manual patrol #2 old way
+ $logs[] = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => 7256,
+ 'log_params' => serialize( [ '6::auto' => false ] ),
+ 'log_timestamp' => 20091223210426
+ ];
+
+ wfGetDB( DB_MASTER )->insert( 'logging', $logs );
+ }
+
+ public function runProvider() {
+ $allRows = [
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => '7251',
+ ],
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => '7252',
+ ],
+ (object)[
+ 'log_type' => 'block',
+ 'log_action' => 'block',
+ 'log_user' => '7253',
+ ],
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => '7253',
+ ],
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => '7254',
+ ],
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => '7255',
+ ],
+ (object)[
+ 'log_type' => 'patrol',
+ 'log_action' => 'patrol',
+ 'log_user' => '7256',
+ ],
+ ];
+
+ $cases = [
+ 'dry run' => [
+ $allRows,
+ [ '--sleep', '0', '--dry-run', '-q' ]
+ ],
+ 'basic run' => [
+ [
+ $allRows[0],
+ $allRows[2],
+ $allRows[3],
+ $allRows[5],
+ $allRows[6],
+ ],
+ [ '--sleep', '0', '-q' ]
+ ],
+ 'run with before' => [
+ [
+ $allRows[0],
+ $allRows[2],
+ $allRows[3],
+ $allRows[4],
+ $allRows[5],
+ $allRows[6],
+ ],
+ [ '--sleep', '0', '--before', '20060123210426', '-q' ]
+ ],
+ 'run with check-old' => [
+ [
+ $allRows[0],
+ $allRows[1],
+ $allRows[2],
+ $allRows[3],
+ $allRows[4],
+ $allRows[6],
+ ],
+ [ '--sleep', '0', '--check-old', '-q' ]
+ ],
+ ];
+
+ foreach ( $cases as $key => $case ) {
+ yield $key . '-batch-size-1' => [
+ $case[0],
+ array_merge( $case[1], [ '--batch-size', '1' ] )
+ ];
+ yield $key . '-batch-size-5' => [
+ $case[0],
+ array_merge( $case[1], [ '--batch-size', '5' ] )
+ ];
+ yield $key . '-batch-size-1000' => [
+ $case[0],
+ array_merge( $case[1], [ '--batch-size', '1000' ] )
+ ];
+ }
+ }
+
+ /**
+ * @dataProvider runProvider
+ */
+ public function testRun( $expected, $args ) {
+ $this->maintenance->loadWithArgv( $args );
+
+ $this->maintenance->execute();
+
+ $remainingLogs = wfGetDB( DB_REPLICA )->select(
+ [ 'logging' ],
+ [ 'log_type', 'log_action', 'log_user' ],
+ [],
+ __METHOD__,
+ [ 'ORDER BY' => 'log_id' ]
+ );
+
+ $this->assertEquals( $expected, iterator_to_array( $remainingLogs, false ) );
+ }
+
+ public function testFromId() {
+ $fromId = wfGetDB( DB_REPLICA )->selectField(
+ 'logging',
+ 'log_id',
+ [ 'log_params' => 'nanana' ]
+ );
+
+ $this->maintenance->loadWithArgv( [ '--sleep', '0', '--from-id', strval( $fromId ), '-q' ] );
+
+ $this->maintenance->execute();
+
+ $remainingLogs = wfGetDB( DB_REPLICA )->select(
+ [ 'logging' ],
+ [ 'log_type', 'log_action', 'log_user' ],
+ [],
+ __METHOD__,
+ [ 'ORDER BY' => 'log_id' ]
+ );
+
+ $deleted = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => '7254',
+ ];
+ $notDeleted = [
+ 'log_type' => 'patrol',
+ 'log_action' => 'autopatrol',
+ 'log_user' => '7252',
+ ];
+
+ $remainingLogs = array_map(
+ function ( $val ) {
+ return (array)$val;
+ },
+ iterator_to_array( $remainingLogs, false )
+ );
+
+ $this->assertNotContains( $deleted, $remainingLogs );
+ $this->assertContains( $notDeleted, $remainingLogs );
+ }
+
+}